主题
Go 结构体与方法
结构体把相关数据放在一起,方法把围绕这些数据的操作放在类型上。真正容易写错的地方,是接收者到底拿到哪一份数据:func (c Counter) 与 func (c *Counter) 只差一个星号,却会影响修改是否留下来、接口能否实现,以及保存方法后会读到哪个值。
这篇从一个计数器开始,依次看接收者、方法集、嵌入、标签和内存布局。先把容易混在一起的几件事拆开:
| 要决定什么 | 看哪个机制 | 本篇会验证的现象 |
|---|---|---|
| 方法能否改到原结构体字段 | 值接收者、指针接收者 | 改结构体副本的整数不生效,改共享 slice 的元素却生效 |
| 某类型能否放进接口 | 方法集 | c.Inc() 能调用,不代表 Counter 实现了 Inc() 接口 |
| 能否少写一层字段名 | 嵌入与提升 | s.Label() 可以来自内嵌类型,但不会自动调用外层“覆盖”的方法 |
| JSON 名称和空字段怎么处理 | struct tag 与消费它的包 | 标签只是元数据,validate:"required" 不会自行检查 |
| 同样字段为什么占用不同字节 | 对齐与填充 | 换字段顺序,本机结构体大小从 24 字节变成 16 字节 |
正文输出在 Go 1.27.2、Linux/amd64 上实际运行。方法与类型关系是语言规则;具体布局数字只对应这个工具链和架构。完整代码和反例都在文末。
先有结构体,再给它定义操作
go
type Counter struct {
N int
Samples []int
}Counter 是一个定义类型,里面有两个字段。字段名首字母大写可以从其他包访问,未导出的字段则限于所属包。结构体字面量可以按字段名赋初值,没写出的字段取零值。[1][2]
| 写法 | 得到什么 | 需要注意什么 |
|---|---|---|
var c Counter | 零值结构体 | N 为 0,Samples 为 nil |
Counter{N: 1} | 一个结构体值 | 没写的 Samples 仍为 nil |
&Counter{N: 1} | 指向结构体的 *Counter | 可以通过指针修改原字段 |
new(Counter) | 指向零值结构体的 *Counter | 不会替你填业务默认值 |
var p *Counter | nil 指针 | 没有自动创建一个 Counter |
这些创建方式的区别来自零值、复合字面量和 new 的规则。&Counter{}、new(Counter) 不意味着“肯定在堆上分配”;这里先讨论类型和可观察行为,具体存储位置交给编译器分析,不由一个 & 决定。[3][4]
方法的接收者写在方法名前面。接收者类型要满足规范要求,其基类型必须在同一个包中定义,不能随手给另一个包的类型追加方法。func (c Counter) Value() int 表示这个操作属于 Counter,调用时用 c.Value()。[5]
值接收者与指针接收者:复制的是结构体,还是指针
给计数器加三个方法:
go
func (c Counter) TryChange() {
c.N++
c.Samples[0]++
}
func (c Counter) Value() int { return c.N }
func (c *Counter) Inc() { c.N++ }| 问题 | 值接收者 func (c Counter) | 指针接收者 func (c *Counter) |
|---|---|---|
| 调用时复制什么 | 结构体值,各字段按值复制 | 一个指针值 |
给 c.N 赋值 | 改接收者副本 | 改指针指向的结构体 |
| 修改字段中的共享 slice/map 内容 | 仍可能影响外部数据 | 也可能影响外部数据 |
| 能否当作“只读方法”的保证 | 不能,引用数据仍可能变 | 不能 |
| 通常适合什么 | 小、可复制、按值理解的对象 | 需要修改原对象,或不应复制的对象 |
值接收者不等于深复制,也不等于只读
go
c := Counter{N: 1, Samples: []int{10}}
c.TryChange()
fmt.Println(c.N, c.Samples)
c.Inc()
fmt.Println(c.N)text
1 [11]
2TryChange 拿到结构体副本,副本的 N 从 1 加到了 2,但原对象的 N 留在 1。Samples 字段是 slice,复制结构体只是复制了 slice 描述,所以两份字段仍指向同一个数组;改第一个样本,原对象就看到了 11。之后的 Inc 拿到原结构体指针,才把原来的 N 改成 2。[6:1]
正在绘制图示…
查看图源代码
flowchart LR
O["调用方 c<br/>N = 1<br/>Samples 描述"] --> A["共享数组<br/>11"]
V["TryChange 的副本<br/>N = 2<br/>Samples 描述"] --> A
P["Inc 的接收者<br/>复制的指针"] --> O
给结构体换一个值接收者,不能自动获得并发安全。如果方法通过 slice、map 或指针字段修改共享对象,仍然要按共享读写处理。
怎么选接收者
需要修改对象状态时,指针接收者最直接。结构体含有 sync.Mutex 等使用后不可复制的成员时,也应避免用值接收者复制它。小而独立的值,例如坐标、范围或已经设计成按值使用的配置,可以考虑值接收者。Go 官方风格建议还强调:同一个类型的方法尽量保持一致,不为了某一个“只读”方法就随意混用。上例混用是为了对照机制,不是推荐的计数器 API。[7][8]
性能上没有“指针一定更快”的通用答案。值接收者有复制成本,指针接收者有共享、间接访问和可能的逃逸代价;这些是否重要,取决于对象大小、调用方式和编译结果。先让修改语义清楚,需要优化时再基准测量。本篇没有用一组微基准替所有结构体下结论。[4:1]
方法集:判断接口实现,不能只看某次调用能否编译
沿用 Value 和 Inc,去掉演示用的 TryChange 不影响下表中的规则:[9]
| 类型 | 包含值接收者 Value() | 包含指针接收者 Inc() |
|---|---|---|
Counter | 是 | 否 |
*Counter | 是 | 是 |
所谓方法集,就是按语言规则,一个类型具有哪些方法。接口要求的方法必须由类型的方法集满足。[9:1]
正在绘制图示…
查看图源代码
flowchart LR
V["func Counter.Value"] --> T["Counter 的方法集"]
V --> P["*Counter 的方法集"]
I["func *Counter.Inc"] --> P
T --> R["满足 Value 接口"]
P --> R
P --> W["满足 Inc 接口"]
为什么 c.Inc() 能调,Counter 却不实现接口
go
type Valuer interface { Value() int }
type Incrementer interface { Inc() }
var _ Valuer = Counter{}
var _ Valuer = (*Counter)(nil)
var _ Incrementer = (*Counter)(nil)
// var _ Incrementer = Counter{} // 编译失败var _ 接口 = 值 是一个编译期实现检查,既不要求真的使用这个变量,也不会调用指针的方法。最后一行如果启用,本次编译器报:
text
Counter does not implement Incrementer (method Inc has pointer receiver)但前面 c.Inc() 明明能运行。原因是 c 是可寻址变量,Go 允许把这种调用写成 (&c).Inc() 的简写。这个调用便利只改变如何取得接收者,没有把 Inc 加入 Counter 的方法集。向接口赋值时,判断的仍然是 Counter 自身的方法集。[10][9:2]
可寻址可以先理解为“这里有一个能取地址的具体变量”。普通局部变量、结构体变量的字段、slice 元素通常可以;map 元素、普通函数返回的临时结构体不能。因此,下面几种写法不能靠自动取址过关:[11]
go
Counter{}.Inc()
m := map[string]Counter{"x": {}}
m["x"].Inc()
_ = Counter.Inc // 这个方法表达式也不存在要修改 map 中的值,取出可寻址的副本、调用、再放回;或者确实需要共享对象时,让 map 存 *Counter。可取址规则与方法集规则要分开判断,更多 map 的值语义见 Go 类型系统。
放进接口的值,仍然遵循复制规则
go
c := Counter{N: 2}
var v Valuer = c
var p Incrementer = &c
p.Inc()
fmt.Println(v.Value(), c.Value())text
2 3v 保存的是当时复制进去的 Counter 值,p 保存的是指向原对象的指针。通过 p 修改原对象以后,v 中独立的整数字段不会跟着变成 3。接口没有抹掉“值还是指针”的区别;完整的动态类型与 nil 行为见 Go 接口。[6:2][12]
把方法保存成函数时,接收者何时确定
方法也可以像普通函数一样传递,但下面两种写法绑定接收者的时机不同。[13]
| 写法 | 名称 | 接收者来自哪里 |
|---|---|---|
c.Value | 方法值 | 创建时求值并保存接收者,之后调用不再传接收者 |
Counter.Value | 方法表达式 | 得到普通函数,调用时显式传第一个参数 Counter |
c.Inc | 方法值 | 此处 c 可寻址,保存的是对应指针接收者 |
(*Counter).Inc | 方法表达式 | 调用时显式传 *Counter |
go
c := Counter{N: 1}
saved := c.Value
inc := c.Inc
c.N = 10
inc()
fmt.Println(saved(), c.Value())
read := Counter.Value
add := (*Counter).Inc
add(&c)
fmt.Println(read(c), (*Counter).Value(&c))
fmt.Printf("%T | %T\n", read, add)text
1 11
12 12
func(main.Counter) int | func(*main.Counter)saved 保存了创建时的 Counter 副本,所以后来把 c.N 改到 10,它仍读出 1。inc 保存的指针仍指向 c,执行时就在当时的 10 上加一。后面的 read(c)、add(&c) 是普通函数调用:接收者被放到第一个显式参数里,因此 %T 打印出两种函数签名。[13:1]
写回调、延迟执行或把方法交给另一层处理时,这个区别会影响结果。若希望执行时读取最新状态,就不要无意中保存一个值接收者的旧副本。
嵌入:把内部字段的方法提升出来,不会建立继承关系
组合最普通的写法是给内部对象一个明确字段名:
go
type Holder struct { Counter Counter }调用时写 h.Counter.Inc()。如果改成匿名嵌入 type Holder struct { Counter },在选择器没有冲突时,可以简写为 h.Inc()。底层仍然有一个名为 Counter 的字段;嵌入的作用之一,是提升字段和方法的可访问路径。[14]
| 方式 | 调用形态 | 主要取舍 |
|---|---|---|
| 命名字段 | h.Counter.Inc() | 依赖关系直接可见,不自动扩大外层方法集 |
| 嵌入字段 | h.Inc() | 减少转发代码,但会把符合规则的方法带到外层 |
| 显式包装方法 | 外层定义自己的 Inc,内部再调用 | 多写一点代码,可控制暴露哪些操作与额外规则 |
“组合优于继承”在这里更适合理解成一种设计建议:先把对象之间的协作写清楚,是否嵌入是下一步选择。不要仅为省几个字符嵌入一个很大的类型,结果让不想暴露的方法也成为 API 的一部分。[14:1]
外层同名方法不会改变内层方法的接收者
go
type Base struct{}
func (Base) Name() string { return "base" }
func (b Base) Label() string { return b.Name() }
type Service struct { Base }
func (Service) Name() string { return "service" }
s := Service{}
fmt.Println(s.Name(), s.Label(), s.Base.Name())text
service base bases.Name() 选中外层自己的方法。s.Label() 提升自 Base,实际接收者仍是 s.Base;进入 Base.Label 后,b.Name() 调用的自然是 Base.Name,不会自动回到外层去找 Service.Name。嵌入没有提供这类继承式动态分派。[14:2][15]
正在绘制图示…
查看图源代码
flowchart LR
A["s.Name()"] --> B["Service.Name<br/>接收者 Service"]
C["s.Label()"] --> D["s.Base.Label()<br/>接收者 Base"]
D --> E["Base.Name()<br/>返回 base"]
还要留意名字冲突:如果两条同样深的嵌入路径都提供 X,s.X 会产生歧义,需要明确写 s.Left.X 或 s.Right.X。文末编译反例检查了这一点。[15:1]
嵌入 T 与嵌入 *T,会得到不同的方法集
在没有冲突和遮蔽的前提下,提升规则如下。ValueEmbedded 与 PointerEmbedded 的定义都在完整代码中。[14:3]
| 外层定义 | 外层值的方法集 | 外层指针的方法集 |
|---|---|---|
struct { Counter } | 提升 Counter 的值接收者方法 | 还提升 *Counter 的指针接收者方法 |
struct { *Counter } | 提升值接收者与指针接收者方法 | 同样提升两类方法 |
go
v := ValueEmbedded{Counter: Counter{N: 3}}
v.Inc()
fmt.Println(v.Value(), v.Counter.N)
p := PointerEmbedded{Counter: &Counter{N: 5}}
var inc Incrementer = p
inc.Inc()
fmt.Println(p.Value())text
4 4
6第一段 v.Inc() 仍利用了可寻址变量的调用便利;ValueEmbedded 值本身不因此实现 Incrementer。第二段则不同:由于嵌入的是 *Counter,PointerEmbedded 值的方法集里就有提升后的 Inc,所以可以直接交给接口。[14:4][10:1]
代价是零值:var p PointerEmbedded 的内嵌指针是 nil,调用这里的 p.Inc() 会在访问 c.N 时 panic。指针接收者方法并非一遇 nil 就由语言禁止调用,它可以自己处理 nil;但本例 Inc 没有这种处理。选指针嵌入时,要明确初始化要求,而不是只看方法集更“大”。文末测试验证了零值调用的 panic。[10:2][15:2]
struct tag:约定给谁看,谁才会处理
字段后面的标签是字符串元数据。语言会把它提供给反射,并把它纳入结构体类型身份的判定;标签本身不会执行校验、转换或数据库操作。reflect 约定的常见格式是由空格分隔的 key:"value" 对,JSON、数据库库、校验库各自读取自己关心的键。[16]
go
type User struct {
Name string `json:"name" validate:"required"`
Count int `json:"count,omitempty"`
Secret string `json:"-"`
note string
}
u := User{Name: "gopher", Secret: "hidden", note: "private"}
b, err := json.Marshal(u)
fmt.Println(string(b), err)
f, _ := reflect.TypeOf(u).FieldByName("Name")
fmt.Println(f.Tag.Get("json"), f.Tag.Get("validate"))text
{"name":"gopher"} <nil>
name required四个字段的处理各有原因:[17]
| 字段 | 为什么这样输出 |
|---|---|
Name | 导出字段,json:"name" 指定 JSON 名称 |
Count | omitempty 会省略这里值为 0 的整数 |
Secret | json:"-" 明确忽略 |
note | 未导出,不作为普通字段编码 |
validate:"required" 只是被反射读出了字符串 required,本例没有校验库消费它。把 User{} 交给 json.Marshal,仍会成功得到 {"name":""},文末测试验证了这个边界。标签写上了,不等于校验已经发生。
要区分“没这个标签”和“标签明确写了空字符串”,使用 Lookup:
go
t := reflect.StructTag(`label:""`)
a, aok := t.Lookup("label")
z, zok := t.Lookup("missing")
fmt.Printf("%q %t | %q %t\n", a, aok, z, zok)text
"" true | "" falseGet 对两种情况都返回空串,Lookup 多返回的布尔值才保留了存在性。标签格式错误时,Get、Lookup 的结果不能依赖,包文档明确写了“未指定”。go vet 能检查常见标签格式错误;文末用 json: "n" 做了故意失败的检查。[16:1]
这里使用的是 encoding/json,不是 encoding/json/v2。复杂嵌入字段、同名标签和序列化细节应继续看对应包的字段选择规则,不能把上面四行表推广成所有嵌入情形。[17:1]
内存对齐:字段大小加起来,不一定等于结构体大小
对齐可以先理解为:一个字段的起始地址要满足该类型要求的边界。为了把字段放到合适的偏移,结构体中可能插入不承载业务值的填充字节,末尾也可能补齐。unsafe.Sizeof 计算结构体自身大小时包括这些填充,但不把 slice、map、指针间接引用的数据一起算进去。[18]
看两个只有字段顺序不同的类型:
go
type Unpacked struct {
A byte
B int64
C byte
}
type Packed struct {
B int64
A byte
C byte
}
var a Unpacked
var b Packed
fmt.Println("unpacked:", unsafe.Sizeof(a), unsafe.Alignof(a),
unsafe.Offsetof(a.A), unsafe.Offsetof(a.B), unsafe.Offsetof(a.C))
fmt.Println("packed:", unsafe.Sizeof(b), unsafe.Alignof(b),
unsafe.Offsetof(b.A), unsafe.Offsetof(b.B), unsafe.Offsetof(b.C))本次 Go 1.27.2、Linux/amd64 输出:
text
unpacked: 24 8 0 8 16
packed: 16 8 8 0 9每行依次是总大小、对齐要求、A/B/C 三个字段的偏移。字段数据只有 1+8+1=10 字节,额外空间来自填充。这个架构上 int64 字段要按 8 字节对齐,第一种布局在 A 后面补 7 字节,C 后面又补 7 字节;第二种把 B 放在最前面,中间不必补,末尾只补 6 字节。[18:1]
正在绘制图示…
查看图源代码
flowchart LR
subgraph U["Unpacked:24 字节"]
U1["A<br/>偏移 0"] --> U2["填充<br/>1—7"] --> U3["B<br/>8—15"] --> U4["C<br/>16"] --> U5["填充<br/>17—23"]
end
subgraph P["Packed:16 字节"]
P1["B<br/>0—7"] --> P2["A、C<br/>8、9"] --> P3["填充<br/>10—15"]
end
这张图描述内存中的排列顺序,箭头不表示程序调用或指针引用。变量名 Packed 也只是为了对照,它没有使用某种取消对齐的“打包”功能。
| 需要看什么 | 工具 | 不要误读成什么 |
|---|---|---|
| 结构体自身多少字节 | unsafe.Sizeof(x) | 不是整个对象关联数据的总内存 |
| 对齐边界 | unsafe.Alignof(x) 或字段形式 | 不是每个平台都固定相同 |
| 字段离结构体起点多远 | unsafe.Offsetof(x.Field) | 不是网络协议或文件格式约定 |
当某类对象数量很大时,减少填充可能值得;对象只有少量实例时,字段可读性可能更重要。不要为了省几个字节随意改外部契约,也不要直接把结构体的原始内存当作跨平台编码。需要稳定格式时,应按协议显式序列化。这些是根据布局依赖平台得出的工程建议,本篇只测了两个类型的静态大小,没有测真实业务的内存或速度收益。[18:2]
写一个结构体 API 时,可以按这个顺序检查:对象是一个可复制的值,还是共享的可变状态;相应方法集能否满足实际接口;嵌入是否暴露了恰当的操作;标签是否真有对应代码消费;布局是否已经重要到值得测量。这样每个选择都有具体理由,不必把所有结构体一律写成指针,也不必为模仿继承而堆嵌入层次。
复现与检查结果
五份文件按下文路径保存后执行:
bash
go version
go run .
go test -count=1 -v .
go test -count=1 -race .
go vet .
go build -o invalid.a testdata/compile_fail.go
go vet testdata/vet_fail.go本次环境为 Go 1.27.2、Linux/amd64、CGO_ENABLED=1。正常程序、4 个 Example、3 个 Test、race、vet 均通过。最后两条是故意失败检查:编译器拒绝错误的方法集和歧义选择器;vet 报出坏标签与复制 mutex。它们都返回退出码 1。布局数字不放进跨平台测试断言。race 只覆盖本次执行路径,本例没有开展并发性能实验。[19]
编译反例的核心诊断包括:
text
Counter does not implement Incrementer (method Inc has pointer receiver)
cannot call pointer method Inc on Counter
invalid method expression Counter.Inc (needs pointer receiver (*Counter).Inc)
ambiguous selector both.Xvet 的核心诊断包括:
text
struct field tag `json: "n"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
copyLock passes lock by value: sync.Mutex诊断后面的类型内部细节、路径和行号可能随工具链变化;下面保留完整反例,便于在自己的版本重新检查。
完整可复现代码
go.mod
go
module example.com/go-structs
go 1.27.0main.go
代码块 · 131 行 · 点击收起 / 展开
go
package main
import (
"encoding/json"
"fmt"
"reflect"
"unsafe"
)
type Counter struct {
N int
Samples []int
}
func (c Counter) TryChange() { c.N++; c.Samples[0]++ }
func (c Counter) Value() int { return c.N }
func (c *Counter) Inc() { c.N++ }
type Valuer interface{ Value() int }
type Incrementer interface{ Inc() }
var _ Valuer = Counter{}
var _ Valuer = (*Counter)(nil)
var _ Incrementer = (*Counter)(nil)
func receivers() {
c := Counter{N: 1, Samples: []int{10}}
c.TryChange()
fmt.Println(c.N, c.Samples)
c.Inc()
fmt.Println(c.N)
var v Valuer = c
var p Incrementer = &c
p.Inc()
fmt.Println(v.Value(), c.Value())
}
func methodFunctions() {
c := Counter{N: 1}
saved := c.Value
inc := c.Inc
c.N = 10
inc()
fmt.Println(saved(), c.Value())
read := Counter.Value
add := (*Counter).Inc
add(&c)
fmt.Println(read(c), (*Counter).Value(&c))
fmt.Printf("%T | %T\n", read, add)
}
type Base struct{}
func (Base) Name() string { return "base" }
func (b Base) Label() string { return b.Name() }
type Service struct{ Base }
func (Service) Name() string { return "service" }
type ValueEmbedded struct{ Counter }
type PointerEmbedded struct{ *Counter }
var _ Valuer = ValueEmbedded{}
var _ Incrementer = (*ValueEmbedded)(nil)
var _ Valuer = PointerEmbedded{}
var _ Incrementer = PointerEmbedded{}
func embedding() {
s := Service{}
fmt.Println(s.Name(), s.Label(), s.Base.Name())
v := ValueEmbedded{Counter: Counter{N: 3}}
v.Inc()
fmt.Println(v.Value(), v.Counter.N)
p := PointerEmbedded{Counter: &Counter{N: 5}}
var inc Incrementer = p
inc.Inc()
fmt.Println(p.Value())
}
type User struct {
Name string `json:"name" validate:"required"`
Count int `json:"count,omitempty"`
Secret string `json:"-"`
note string
}
func tags() {
u := User{Name: "gopher", Secret: "hidden", note: "private"}
b, err := json.Marshal(u)
fmt.Println(string(b), err)
f, _ := reflect.TypeOf(u).FieldByName("Name")
fmt.Println(f.Tag.Get("json"), f.Tag.Get("validate"))
t := reflect.StructTag(`label:""`)
a, aok := t.Lookup("label")
z, zok := t.Lookup("missing")
fmt.Printf("%q %t | %q %t\n", a, aok, z, zok)
}
type Unpacked struct {
A byte
B int64
C byte
}
type Packed struct {
B int64
A byte
C byte
}
func layout() {
var a Unpacked
var b Packed
fmt.Println("unpacked:", unsafe.Sizeof(a), unsafe.Alignof(a),
unsafe.Offsetof(a.A), unsafe.Offsetof(a.B), unsafe.Offsetof(a.C))
fmt.Println("packed:", unsafe.Sizeof(b), unsafe.Alignof(b),
unsafe.Offsetof(b.A), unsafe.Offsetof(b.B), unsafe.Offsetof(b.C))
}
func main() {
for _, demo := range []struct {
name string
run func()
}{
{"receivers", receivers}, {"method functions", methodFunctions},
{"embedding", embedding}, {"tags", tags}, {"layout", layout},
} {
fmt.Println("==", demo.name, "==")
demo.run()
}
}main_test.go
代码块 · 58 行 · 点击收起 / 展开
go
package main
import (
"encoding/json"
"testing"
)
func Example_receivers() {
receivers()
// Output:
// 1 [11]
// 2
// 2 3
}
func Example_methodFunctions() {
methodFunctions()
// Output:
// 1 11
// 12 12
// func(main.Counter) int | func(*main.Counter)
}
func Example_embedding() {
embedding()
// Output:
// service base base
// 4 4
// 6
}
func Example_tags() {
tags()
// Output:
// {"name":"gopher"} <nil>
// name required
// "" true | "" false
}
func TestNilEmbeddedPointer(t *testing.T) {
var p PointerEmbedded
defer func() {
if recover() == nil {
t.Fatal("expected nil dereference")
}
}()
p.Inc()
}
func TestTagDoesNotValidate(t *testing.T) {
b, err := json.Marshal(User{})
if err != nil || string(b) != `{"name":""}` {
t.Fatalf("got %s, %v", b, err)
}
}
func TestNamedComposition(t *testing.T) {
type Holder struct{ Counter Counter }
h := Holder{Counter: Counter{N: 2}}
h.Counter.Inc()
if h.Counter.Value() != 3 {
t.Fatal("named field not updated")
}
}testdata/compile_fail.go
go
package fail
type Counter struct{ N int }
func (c *Counter) Inc() { c.N++ }
type Incrementer interface{ Inc() }
var _ Incrementer = Counter{}
type Left struct{ X int }
type Right struct{ X int }
type Both struct {
Left
Right
}
func invalid() {
Counter{}.Inc()
m := map[string]Counter{"x": {}}
m["x"].Inc()
_ = Counter.Inc
var both Both
_ = both.X
}testdata/vet_fail.go
go
package fail
import "sync"
type BadTag struct {
N int `json: "n"`
}
func copyLock(mu sync.Mutex) { mu.Lock(); mu.Unlock() }依据
语言规则与本次 Go 1.27.2 官方发行包中的 doc/go_spec.html 核对;标准库行为另用同版本 go doc 检查。实测布局仅描述 Linux/amd64。
Go FAQ:如何知道变量分配在栈还是堆。存储位置与逃逸和实现有关,不由源代码中的
new或&单独决定。 ↩︎ ↩︎Go 官方 Wiki:Receiver Type,给出接收者选择和保持一致的建议;建议与语言允许的范围不同。 ↩︎
sync.Mutex:首次使用后不得复制;go vet的 copylocks 与 structtag 检查。 ↩︎Go 规范:变量的静态与动态类型、接口类型。 ↩︎
Go 规范:结构体嵌入及提升的方法集;Effective Go 的嵌入说明,解释接收者仍是内部对象,组合不同于继承。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Go 规范:结构体标签;
reflect.StructTag、Get、Lookup,定义标签约定及错误格式的未指定结果。 ↩︎ ↩︎encoding/json.Marshal:导出字段、字段标签、omitempty、忽略字段及嵌入字段选择规则。这里引用 v1 包的约定。 ↩︎ ↩︎unsafe.Sizeof、Alignof、Offsetof;Go 规范:大小与对齐保证。具体字段布局由本文 Go 1.27.2 实测补充,不把 amd64 数字推广到其他架构。 ↩︎ ↩︎ ↩︎Go 数据竞争检测器:支持环境、命令和覆盖限制。 ↩︎